I originally wrote the following for my Chainline Newsletter, but I continue to get tweets about this idea, so I'm re-publishing the article here on my blog. This version has been lightly edited.
我最初为我的 Chainline Newsletter 撰写了以下内容,但由于我持续收到关于这个想法的推文,所以我决定在我的博客上重新发布这篇文章。这个版本已经做了轻微的编辑。
I've been thinking about the consequences of the "wrong abstraction." My RailsConf 2014 "all the little things" talk included a section where I asserted:
我一直思考"错误抽象"的后果。我在 RailsConf 2014 的"所有的小事"演讲中包含了一个部分,在那里我断言:
duplication is far cheaper than the wrong abstraction
重复远比错误抽象便宜
And in the summary, I went on to advise:
在总结中,我继续建议:
prefer duplication over the wrong abstraction
宁愿选择重复也不愿采用错误抽象
This small section of a much bigger talk invoked a surprisingly strong reaction. A few folks suggested that I had lost my mind, but many more expressed sentiments along the lines of:
这段话虽然只是更大谈话中的一小部分,却引发了出人意料的强烈反响。有些人认为我失去了理智,但更多的人表达了类似这样的观点:
This, a million times this! "@BonzoESC: "Duplication is far cheaper than the wrong abstraction" @sandimetz @rbonales pic.twitter.com/3qMI0waqWb"
正是如此!一百万次都是如此!" @BonzoESC: "重复远比错误抽象便宜" @sandimetz @rbonales pic.twitter.com/3qMI0waqWb"
— 41 shades of blue (@pims) March 7, 2014
— 41 shades of blue (@pims) 2014 年 3 月 7 日
The strength of the reaction made me realize just how widespread and intractable the "wrong abstraction" problem is. I started asking questions and came to see the following pattern:
反应的强烈程度让我意识到"错误抽象"问题有多么普遍和难以解决。我开始提问并发现了以下模式:
Programmer A sees duplication.
程序员 A 发现了重复。Programmer A extracts duplication and gives it a name.
程序员 A 提取了重复并为其命名。This creates a new abstraction. It could be a new method, or perhaps even a new class.
这创造了一个新的抽象。它可能是一个新的方法,甚至可能是一个新的类。Programmer A replaces the duplication with the new abstraction.
程序员 A 用新的抽象替换了重复部分。Ah, the code is perfect. Programmer A trots happily away.
啊,代码完美了。程序员 A 愉快地离开了。Time passes. 时间流逝。
A new requirement appears for which the current abstraction is almost perfect.
出现了一个新需求,而当前的抽象几乎完美。Programmer B gets tasked to implement this requirement.
程序员 B 被分配了这个任务。Programmer B feels honor-bound to retain the existing abstraction, but since isn't exactly the same for every case, they alter the code to take a parameter, and then add logic to conditionally do the right thing based on the value of that parameter.
程序员 B 觉得有责任保留现有的抽象,但由于它并不适用于每个情况,他们修改了代码以接受一个参数,然后根据该参数的值添加逻辑来条件性地做正确的事情。What was once a universal abstraction now behaves differently for different cases.
曾经是一个通用的抽象现在对不同的情况表现出不同的行为。Another new requirement arrives.
又来了一个新的需求。
Programmer X. 程序员 X。
Another additional parameter.
另一个附加参数。
Another new conditional. 另一个新的条件。
Loop until code becomes incomprehensible.
循环直到代码变得难以理解。You appear in the story about here, and your life takes a dramatic turn for the worse.
你出现在故事中的这个位置,你的生活突然急转直下。
Existing code exerts a powerful influence. Its very presence argues that it is both correct and necessary. We know that code represents effort expended, and we are very motivated to preserve the value of this effort. And, unfortunately, the sad truth is that the more complicated and incomprehensible the code, i.e. the deeper the investment in creating it, the more we feel pressure to retain it (the "sunk cost fallacy"). It's as if our unconscious tell us "Goodness, that's so confusing, it must have taken ages to get right. Surely it's really, really important. It would be a sin to let all that effort go to waste."
现有的代码具有强大的影响力。它的存在本身就证明了它是正确且必要的。我们知道代码代表着付出的努力,我们非常愿意保留这种努力的价值。不幸的是,令人悲伤的真相是,代码越复杂、越难理解,即创建它的投入越深,我们越感到有压力要保留它(即"沉没成本谬误")。就好像我们的潜意识告诉我们:"天哪,这太复杂了,一定花了很多时间去弄对。它肯定非常重要。如果让所有这些努力白费了,那将是罪恶。"
When you appear in this story in step 8 above, this pressure may compel you to proceed forward, that is, to implement the new requirement by changing the existing code. Attempting to do so, however, is brutal. The code no longer represents a single, common abstraction, but has instead become a condition-laden procedure which interleaves a number of vaguely associated ideas. It is hard to understand and easy to break.
在上述第 8 步中,当你出现在这个故事时,这种压力可能会迫使你继续前进,也就是说,通过修改现有代码来实现新需求。然而,尝试这样做却是残酷的。代码不再代表单一的、通用的抽象,而是变成了一个充满条件的程序,交织着许多模糊关联的想法。它难以理解,也容易出错。
If you find yourself in this situation, resist being driven by sunk costs. When dealing with the wrong abstraction, the fastest way forward is back. Do the following:
如果你发现自己处于这种情况,要抵制被沉没成本所驱动的诱惑。当处理错误抽象时,前进最快的方法是退回。请执行以下操作:
- Re-introduce duplication by inlining the abstracted code back into every caller.
通过将抽象代码重新内联到每个调用者中,重新引入重复。 - Within each caller, use the parameters being passed to determine the subset of the inlined code that this specific caller executes.
在每个调用者中,使用传递的参数来确定这个特定调用者执行的代码子集。 - Delete the bits that aren't needed for this particular caller.
删除这个特定调用者不需要的部分。
This removes both the abstraction and the conditionals, and reduces each caller to only the code it needs. When you rewind decisions in this way, it's common to find that although each caller ostensibly invoked a shared abstraction, the code they were running was fairly unique. Once you completely remove the old abstraction you can start anew, re-isolating duplication and re-extracting abstractions.
这既消除了抽象,也消除了条件语句,并将每个调用者简化为仅包含其所需代码。以这种方式回溯决策时,通常会发现尽管每个调用者表面上调用了共享的抽象,但它们运行的代码相当独特。一旦完全移除旧的抽象,你就可以重新开始,重新隔离重复并重新提取抽象。
I've seen problems where folks were trying valiantly to move forward with the wrong abstraction, but having very little success. Adding new features was incredibly hard, and each success further complicated the code, which made adding the next feature even harder. When they altered their point of view from "I must preserve our investment in this code" to "This code made sense for a while, but perhaps we've learned all we can from it," and gave themselves permission to re-think their abstractions in light of current requirements, everything got easier. Once they inlined the code, the path forward became obvious, and adding new features become faster and easier.
我曾见过一些人努力地使用错误的抽象向前推进,但收效甚微。添加新功能极其困难,每次成功都使代码更加复杂,这又让添加下一个功能更加困难。当他们从“我必须保护我们对这段代码的投资”转变为“这段代码曾经有意义,但我们可能已经从它中学到了所有能学到的”并允许根据当前需求重新思考抽象时,一切都变得容易起来。一旦他们内联了代码,前进的道路就变得清晰,添加新功能也变得更快更容易。
The moral of this story? Don't get trapped by the sunk cost fallacy. If you find yourself passing parameters and adding conditional paths through shared code, the abstraction is incorrect. It may have been right to begin with, but that day has passed. Once an abstraction is proved wrong the best strategy is to re-introduce duplication and let it show you what's right. Although it occasionally makes sense to accumulate a few conditionals to gain insight into what's going on, you'll suffer less pain if you abandon the wrong abstraction sooner rather than later.
这个故事告诉我们什么?不要陷入沉没成本谬误的陷阱。如果你发现自己通过共享代码传递参数并添加条件路径,那么这个抽象就是错误的。它最初可能是正确的,但现在已经不是了。一旦一个抽象被证明是错误的,最好的策略是重新引入重复代码,让它告诉你什么是正确的。虽然偶尔积累一些条件语句来了解正在发生的事情是有意义的,但如果你尽早放弃错误的抽象,你会少受很多痛苦。
When the abstraction is wrong, the fastest way forward is back. This is not retreat, it's advance in a better direction. Do it. You'll improve your own life, and the lives of all who follow.
当抽象错误时,前进最快的方式是后退。这不是撤退,而是在更好的方向上前进。去做吧。你会改善你自己的生活,以及所有跟随你的人的生活。
News: 99 Bottles of OOP in JS, PHP, and Ruby!
新闻:JavaScript、PHP 和 Ruby 中的 99 个面向对象编程实例!
The 2nd Edition of 99 Bottles of OOP has been released!
《99 个面向对象编程》的第二版已经发布了!
The 2nd Edition contains 3 new chapters and is about 50% longer than the 1st. Also, because 99 Bottles of OOP is about object-oriented design in general rather than any specific language, this time around we created separate books that are technically identical, but use different programming languages for the examples.
第二版包含 3 个新章节,篇幅比第一版长约 50%。此外,由于《99 个面向对象设计》主要讲述通用面向对象设计而非特定语言,这次我们创建了技术内容完全相同但使用不同编程语言的独立书籍。
99 Bottles of OOP is currently available in Ruby, JavaScript, and PHP versions, and beer and milk beverages. It's delivered in epub, kepub, mobi and pdf formats. This results in six different books and (3x2x4) 24 possible downloads; all unique, yet still the same. One purchase gives you rights to download any or all.
《99 个面向对象设计》目前提供 Ruby、JavaScript 和 PHP 版本,以及啤酒和牛奶饮料版本。它以 epub、kepub、mobi 和 pdf 格式交付。这产生了六种不同的书籍和(3x2x4)24 种可能的下载方式;所有版本都是独特的,但内容仍然相同。一次购买即可获得下载任何或所有版本的权限。